淺談原生 component 自創標籤,以<god-button>為例

2021-06-23 Wed

Why component

Can I Use Web Component

藉由組件的封裝減少頁面上各個元素相互影響。

物件導向三大特性

封裝、繼承、多型

以現實生活舉例:電風扇、手槍、散彈槍、步槍。

瀏覽器開啟 shadowDom

shadowmMDN

關於 shadowDom

input 也是屬於一種 shadowDom 如下圖

可更改的 shadowDom 可附加 shadowm

MDN 的內容 使用自定義元素

自定義標籤一定槓槓 例如 MDN 註解如下

class WordCount extends HTMLParagraphElement {
  constructor() {
    // Always call super first in constructor
    super();
    // Element functionality written in here

    ...
  }
}

關於 super()小知識

  • 子 class 需要在 constructor()中呼叫 super()來呼叫父層的建構函式
  • super()只能在 constructor()中執行
  • 子類別的 constructor()呼叫 super 之前,this 是沒有指向的,會跑出 Refference Error

建立一個自創按鈕並撰寫 CSS

來看 Example1

<god-button>

</god-button>
// 建立按鈕元件
class ohMyGodButton extends HTMLElement {
    constructor(){
        super();
        //影子元件開啟
        this.attachShadow({mode:"open"});
        //建立一個"喔我的按鈕"div
        this.ohMyBtn=document.createElement("div");
        //增加CSS樣式(但是這樣不夠優雅)
        this.ohMyBtn.style.display="inline-block";
        this.ohMyBtn.style.backgroundColor="blue";
        this.ohMyBtn.style.color="yellow";
        this.ohMyBtn.style.padding="8px";
        //添加按鈕內的字
        this.ohMyBtn.textContent ="我的按鈕";
        this.shadowRoot.appendChild(this.ohMyBtn);
    }
}

//組件的外部 也就是<自己取的名字></自己取的名字>
window.customElements.define("god-button",ohMyGodButton);

透過直接撰寫 style 然後在裡面寫一般的常見的 CSS 寫法優雅多了。

用建立一個 < style>標籤的方式撰寫(比較優雅唷)

來看 Example2

// 建立按鈕組件// 建立按鈕組件

class ohMyGodButton extends HTMLElement {
    constructor(){
    super();
    //影子元件開啟
    this.attachShadow({mode:"open"});

    //建立一個"喔我的按鈕"div
    this.ohMyBtn=document.createElement("div");

    //增加CSS樣式
    this.ohMyStyle =document.createElement("style");
    this.ohMyStyle.textContent =`
        .btn{
            display:inline-block;
            color:yellow;
            background-color:blue;
            padding:6px;
        }
    `;
    //添加按鈕內的字
    this.ohMyBtn.textContent ="我的按鈕";
    //放入style標籤到shadow底下
    this.shadowRoot.appendChild(this.ohMyStyle);
     //建立一個CSS的class名叫btn
    this.ohMyBtn.className ="btn";
    //放入該div標籤到shadow底下
    this.shadowRoot.appendChild(this.ohMyBtn);
  }
}
//元件的外部 也就是<自己取的名字></自己取的名字>
window.customElements.define("god-button",ohMyGodButton);

來看 Example3

互不衝突

當我在外面建立一個按鈕而且名字也叫 btn 的 class 的時候並不會影響 god-button 裡面的元素

當我在裡面添加一個 class 名叫做 red 把元素樣式寫在外面的時候也不會影響

來看 Example4

模仿 react 的寫法

<god-button></god-button>
<button class="btn">我的按鈕</button>

  // 建立按鈕元件
  class ohMyGodButton extends HTMLElement {
      static style = `
          .btn{
              display:inline-block;
              color:yellow;
              background-color:blue;
              padding:6px;
          }
      `
      constructor(){
          super();
          //影子組件開啟
          this.attachShadow({mode:"open"});
          this.styling();
          this.render();
      }
      styling(){
          //放入style標籤到shadow底下
          this.ohMyStyle =document.createElement("style");
          this.ohMyStyle.textContent = this.constructor.style;
          this.shadowRoot.appendChild(this.ohMyStyle);
      }
      render(){
          //建立一個"喔我的按鈕"div
          this.ohMyBtn=document.createElement("div");
          //添加按鈕內的字
          this.ohMyBtn.textContent ="我的按鈕";
          //建立一個CSS的class名叫btn
          this.ohMyBtn.className ="btn";
          //放入該div標籤到shadow底下
          this.shadowRoot.appendChild(this.ohMyBtn);
      }
  }
  //組件的外部 也就是<自己取的名字></自己取的名字>
  window.customElements.define("god-button",ohMyGodButton);

來看 Example5

為何屬性有改變但是畫面卻沒有變化?

<body>
    <god-button id="myID"></god-button>
    <button class="btn">我的按鈕</button>
<script>


    // 建立按鈕元件
    class ohMyGodButton extends HTMLElement {
        static style = `
            .btn{
                display:inline-block;
                color:yellow;
                background-color:blue;
                padding:6px;
            }
        `
        constructor(){
            super();
            //影子組件開啟
            let shadow =this.attachShadow({mode:"open"});
            this.styling();
            this.render();
            // console.log(this.shadowRoot);
            console.log(shadow===this.shadowRoot);
        }

        styling(){
            //放入style標籤到shadow底下
            this.ohMyStyle =document.createElement("style");
            this.ohMyStyle.textContent = this.constructor.style;
            this.shadowRoot.appendChild(this.ohMyStyle);
        }
        render(){
            //建立一個"喔我的按鈕"div
            this.ohMyBtn=document.createElement("div");
            //建立一個CSS的class名叫btn
            this.ohMyBtn.className ="btn";
            //得到按鈕的屬性叫做txt並寫入到按鈕的文字當中
            this.ohMyBtn.setAttribute("txt","哈哈");
            this.ohMyBtn.textContent=this.ohMyBtn.getAttribute("txt");
            //放入該div標籤到shadow底下
            this.shadowRoot.appendChild(this.ohMyBtn);
        }

    }
    //組件的外部 也就是<自己取的名字></自己取的名字>
    window.customElements.define("god-button",ohMyGodButton);
    let getBtn = document.querySelector(".btn");

    getBtn.addEventListener('click',function(e){
        const getMyID = document.getElementById('myID');
        getMyID.setAttribute('txt','QQ按到我了');
    })

</script>
</body>

來看 Example6

透過外部元件更改屬性的時候,實際上屬性的值是有改變,但是樣式沒有切換 必須撰寫自定義監聽屬性以及當發生屬性被改變時候所執行的函式

參見生命週期

生命週期 MDN MDN 生命週期範例

需要的是,如果在元素屬性變化後,需要觸發 attributeChangedCallback 這個函式的時候,你必須這個監聽這個。可以通過定義 observedAttributes()獲取函式來實現,observedAttributes()函式可以包含一個回傳語句,回傳一個 數組,包含了需要監聽的屬性名稱:

參見第 11 行和第 26 行

console.log(shadow===this.shadowRoot);

// 建立按鈕元件
class ohMyGodButton extends HTMLElement {
    static style = `
        .btn{
            display:inline-block;
            color:yellow;
            background-color:blue;
            padding:6px;
        }
    `
    //監控txt
    static get observedAttributes(){
        return ['txt'];
    }
    constructor(){
        super();
        //影子組件開啟
        let shadow =this.attachShadow({mode:"open"});
        this.styling();
        this.render();
        // console.log(this.shadowRoot);
        console.log(shadow===this.shadowRoot);

    }
    //當屬性被改變的時候執行的函式
    attributeChangedCallback(name,oldValue,newValue){
        // console.log(name,oldValue,newValue);
        this.render();
    }
    styling(){
        //放入style標籤到shadow底下
        this.ohMyStyle =document.createElement("style");
        this.ohMyStyle.textContent = this.constructor.style;
        this.shadowRoot.appendChild(this.ohMyStyle);
    }
    render(){
        if(this.ohMyBtn){
            this.ohMyBtn.remove();
        }
        //建立一個"喔我的按鈕"div
        this.ohMyBtn=document.createElement("div");
        //建立一個CSS的class名叫btn
        this.ohMyBtn.className ="btn pink";
        //得到按鈕的屬性叫做txt並寫入到按鈕的文字當中
        this.ohMyBtn.textContent=this.getAttribute("txt");
        //放入該div標籤到shadow底下
        this.shadowRoot.appendChild(this.ohMyBtn);
    }
}
//組件的外部 也就是<自己取的名字></自己取的名字>
window.customElements.define("god-button",ohMyGodButton);
let getBtn = document.querySelector(".btn");
getBtn.addEventListener('click',function(e){
    const getMyID = document.getElementById('myID');
    getMyID.setAttribute('txt','QQ按到我了');
})

其他

內部也可以 link 外部 CSS

在 vue 裡面使用原生 web component

在 react 裡面使用原生 web component

A Complete Introduction to Web Components in 2022